// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type * as TApiExtractor from '@microsoft/api-extractor';
import type {
  IHeftTaskPlugin,
  IHeftTaskRunHookOptions,
  IHeftTaskSession,
  HeftConfiguration,
  IHeftTaskRunIncrementalHookOptions,
  ConfigurationFile
} from '@rushstack/heft';

import { invokeApiExtractorAsync } from './ApiExtractorRunner';
import apiExtractorConfigSchema from './schemas/api-extractor-task.schema.json';

// eslint-disable-next-line @rushstack/no-new-null
const UNINITIALIZED: null = null;

const PLUGIN_NAME: string = 'api-extractor-plugin';
const TASK_CONFIG_RELATIVE_PATH: string = './config/api-extractor-task.json';
const EXTRACTOR_CONFIG_FILENAME: typeof TApiExtractor.ExtractorConfig.FILENAME = 'api-extractor.json';
const LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./${EXTRACTOR_CONFIG_FILENAME}`;
const EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./config/${EXTRACTOR_CONFIG_FILENAME}`;

const API_EXTRACTOR_CONFIG_SPECIFICATION: ConfigurationFile.IProjectConfigurationFileSpecification<IApiExtractorTaskConfiguration> =
  {
    projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH,
    jsonSchemaObject: apiExtractorConfigSchema
  };

export interface IApiExtractorConfigurationResult {
  apiExtractorPackage: typeof TApiExtractor;
  apiExtractorConfiguration: TApiExtractor.ExtractorConfig;
}

export interface IApiExtractorTaskConfiguration {
  /**
   * If set to true, use the project's TypeScript compiler version for API Extractor's
   * analysis. API Extractor's included TypeScript compiler can generally correctly
   * analyze typings generated by older compilers, and referencing the project's compiler
   * can cause issues. If issues are encountered with API Extractor's included compiler,
   * set this option to true.
   *
   * This corresponds to API Extractor's `--typescript-compiler-folder` CLI option and
   * `IExtractorInvokeOptions.typescriptCompilerFolder` API option. This option defaults to false.
   */
  useProjectTypescriptVersion?: boolean;

  /**
   * If set to true, do a full run of api-extractor on every build.
   */
  runInWatchMode?: boolean;

  /**
   * Controls whether API Extractor prints a diff of the API report file if it's changed.
   * If set to `"production"`, this will only be printed if Heft is run in `--production`
   * mode, and if set to `"always"`, this will always be printed if the API report is changed.
   * This corresponds to API Extractor's `IExtractorInvokeOptions.printApiReportDiff` API option.
   * This option defaults to `"never"`.
   */
  printApiReportDiff?: 'production' | 'always' | 'never';
}

export default class ApiExtractorPlugin implements IHeftTaskPlugin {
  private _apiExtractor: typeof TApiExtractor | undefined;
  private _apiExtractorConfigurationFilePath: string | undefined | typeof UNINITIALIZED = UNINITIALIZED;
  private _printedWatchWarning: boolean = false;

  public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void {
    const runAsync = async (
      runOptions: IHeftTaskRunHookOptions & Partial<IHeftTaskRunIncrementalHookOptions>
    ): Promise<void> => {
      const result: IApiExtractorConfigurationResult | undefined =
        await this._getApiExtractorConfigurationAsync(taskSession, heftConfiguration);
      if (result) {
        await this._runApiExtractorAsync(
          taskSession,
          heftConfiguration,
          runOptions,
          result.apiExtractorPackage,
          result.apiExtractorConfiguration
        );
      }
    };

    taskSession.hooks.run.tapPromise(PLUGIN_NAME, runAsync);
    taskSession.hooks.runIncremental.tapPromise(PLUGIN_NAME, runAsync);
  }

  private async _getApiExtractorConfigurationFilePathAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration
  ): Promise<string | undefined> {
    if (this._apiExtractorConfigurationFilePath === UNINITIALIZED) {
      this._apiExtractorConfigurationFilePath =
        await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(EXTRACTOR_CONFIG_RELATIVE_PATH);
      if (this._apiExtractorConfigurationFilePath === undefined) {
        this._apiExtractorConfigurationFilePath =
          await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(
            LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH
          );
        if (this._apiExtractorConfigurationFilePath !== undefined) {
          taskSession.logger.emitWarning(
            new Error(
              `The "${LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH}" configuration file path is not supported ` +
                `in Heft. Please move it to "${EXTRACTOR_CONFIG_RELATIVE_PATH}".`
            )
          );
        }
      }
    }
    return this._apiExtractorConfigurationFilePath;
  }

  private async _getApiExtractorConfigurationAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration,
    ignoreMissingEntryPoint?: boolean
  ): Promise<IApiExtractorConfigurationResult | undefined> {
    // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json
    // including support for rig.json.  However, Heft does not load the @microsoft/api-extractor package at all
    // unless it sees a config/api-extractor.json file.  Thus we need to do our own lookup here.
    const apiExtractorConfigurationFilePath: string | undefined =
      await this._getApiExtractorConfigurationFilePathAsync(taskSession, heftConfiguration);
    if (!apiExtractorConfigurationFilePath) {
      return undefined;
    }

    // Since the config file exists, we can assume that API Extractor is available. Attempt to resolve
    // and import the package. If the resolution fails, a helpful error is thrown.
    const apiExtractorPackage: typeof TApiExtractor = await this._getApiExtractorPackageAsync(
      taskSession,
      heftConfiguration
    );
    const apiExtractorConfigurationObject: TApiExtractor.IConfigFile =
      apiExtractorPackage.ExtractorConfig.loadFile(apiExtractorConfigurationFilePath);

    // Load the configuration file. Always load from scratch.
    const apiExtractorConfiguration: TApiExtractor.ExtractorConfig =
      apiExtractorPackage.ExtractorConfig.prepare({
        ignoreMissingEntryPoint,
        configObject: apiExtractorConfigurationObject,
        configObjectFullPath: apiExtractorConfigurationFilePath,
        packageJsonFullPath: `${heftConfiguration.buildFolderPath}/package.json`,
        projectFolderLookupToken: heftConfiguration.buildFolderPath
      });

    return { apiExtractorPackage, apiExtractorConfiguration };
  }

  private async _getApiExtractorPackageAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration
  ): Promise<typeof TApiExtractor> {
    if (!this._apiExtractor) {
      const apiExtractorPackagePath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync(
        '@microsoft/api-extractor',
        taskSession.logger.terminal
      );
      this._apiExtractor = (await import(apiExtractorPackagePath)) as typeof TApiExtractor;
    }
    return this._apiExtractor;
  }

  private async _runApiExtractorAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration,
    runOptions: IHeftTaskRunHookOptions & Partial<IHeftTaskRunIncrementalHookOptions>,
    apiExtractor: typeof TApiExtractor,
    apiExtractorConfiguration: TApiExtractor.ExtractorConfig
  ): Promise<void> {
    const {
      runInWatchMode,
      useProjectTypescriptVersion,
      printApiReportDiff: printApiReportDiffOption
    } = (await heftConfiguration.tryLoadProjectConfigurationFileAsync(
      API_EXTRACTOR_CONFIG_SPECIFICATION,
      taskSession.logger.terminal
    )) ?? {};

    if (runOptions.requestRun) {
      if (!runInWatchMode) {
        if (!this._printedWatchWarning) {
          this._printedWatchWarning = true;
          taskSession.logger.terminal.writeWarningLine(
            "API Extractor isn't currently enabled in watch mode."
          );
        }
        return;
      }
    }

    let typescriptPackagePath: string | undefined;
    if (useProjectTypescriptVersion) {
      typescriptPackagePath = await heftConfiguration.rigPackageResolver.resolvePackageAsync(
        'typescript',
        taskSession.logger.terminal
      );
    }

    const production: boolean = taskSession.parameters.production;
    const printApiReportDiff: boolean =
      printApiReportDiffOption === 'always' || (printApiReportDiffOption === 'production' && production);

    // Run API Extractor
    await invokeApiExtractorAsync({
      apiExtractor,
      apiExtractorConfiguration,
      typescriptPackagePath,
      buildFolder: heftConfiguration.buildFolderPath,
      production,
      scopedLogger: taskSession.logger,
      printApiReportDiff
    });
  }
}
